package cc.owoo.godpen.network.http;

import cc.owoo.godpen.analysis.json.Json;
import cc.owoo.godpen.network.reptile.ProxyPool;
import cc.owoo.godpen.network.throughput.OverallThroughput;
import cc.owoo.godpen.util.Path;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;

/**
 * HTTP请求客户端
 * Created by nimensei
 * 2022-02-24 下午 06:41
 **/
public class HttpClient extends AbstractRequest {
    {
        setHeader("user-agent", UserAgent.EDGE);
        setHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9");
        setHeader("accept-encoding", "gzip,deflate");
        setHeader("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
        setHeader("connection", "keep-alive");
    }

    private int timeout = 30000;// 连接超时时长
    private cc.owoo.godpen.network.reptile.ProxyPool proxyPool;// 代理池
    private cc.owoo.godpen.network.http.RequestCache requestCache;// 请求缓存
    private boolean isRecordCookie;// 是否记录cookie

    private OverallThroughput throughput;// 吞吐量（读写流量）

    /**
     * 获取吞吐量计数器
     *
     * @return 吞吐量计数器
     */
    public OverallThroughput createThroughput() {
        if (throughput == null)
            throughput = new OverallThroughput();
        return throughput;
    }

    /**
     * 设置代理池
     *
     * @param proxyPool 代理池对象
     */
    public void setProxyPool(cc.owoo.godpen.network.reptile.ProxyPool proxyPool) {
        this.proxyPool = proxyPool;
    }

    /**
     * 获取代理对象池
     *
     * @return 代理池对象
     */
    public ProxyPool getProxyPool() {
        return proxyPool;
    }

    /**
     * 设置请求缓存
     *
     * @param requestCache 请求缓存对象
     */
    public void setRequestCache(cc.owoo.godpen.network.http.RequestCache requestCache) {
        this.requestCache = requestCache;
    }

    /**
     * 获取请求缓存对象
     *
     * @return 请求缓存对象
     */
    public RequestCache getRequestCache() {
        return requestCache;
    }

    /**
     * 设置是否记录cookie
     *
     * @param recordCookie 是否记录cookie
     */
    public void setRecordCookie(boolean recordCookie) {
        isRecordCookie = recordCookie;
    }

    /**
     * 是否记录cookie
     *
     * @return 是否记录cookie
     */
    public boolean isRecordCookie() {
        return isRecordCookie;
    }

    /**
     * 创建请求信息对象
     *
     * @param request 请求对象
     * @return 请求信息对象
     */
    public cc.owoo.godpen.network.http.RequestInfo createRequestInfo(Request request) {
        cc.owoo.godpen.network.http.RequestInfo info = new cc.owoo.godpen.network.http.RequestInfo();
        info.agreement = request.agreement == null ? agreement : request.agreement;
        if (info.agreement == null)
            info.agreement = "http";
        info.host = request.host == null ? host : request.host;
        info.port = request.port == -1 ? port : request.port;
        info.copySetRequest(request);
        info.content = request.content;
        info.port = info.getPort();
        info.path.set(path);
        info.path.appendLast(request.path);
        appendHeader(request, info);
        return info;
    }


    /**
     * 建立连接并且返回Response对象
     *
     * @param url 请求地址
     * @return Response对象
     */
    public Response connect(String url) {
        return connect(url, null);
    }

    /**
     * 建立连接并且返回Response对象
     *
     * @param url      请求地址
     * @param response 响应对象
     * @return Response对象
     */
    public Response connect(String url, Response response) {
        if (url == null)
            return null;
        return connect(new Request() {{
            setUrl(url);
        }}, response);
    }


    /**
     * 建立连接并且返回Response对象
     *
     * @param request 请求对象
     * @return Response对象
     */
    public Response connect(Request request) {
        return connect(request, null);
    }

    /**
     * 建立连接并且返回Response对象
     *
     * @param request  请求对象
     * @param response 响应对象
     * @return Response对象
     */
    public Response connect(Request request, Response response) {
        return connect(createRequestInfo(request), response);
    }

    /**
     * 建立连接并且返回Response对象
     *
     * @param info 请求对象
     * @return Response对象
     */
    public Response connect(cc.owoo.godpen.network.http.RequestInfo info) {
        return connect(info, null);
    }

    /**
     * 建立连接并且返回Response对象
     *
     * @param info     请求对象
     * @param response 响应对象
     * @return Response对象
     */
    public Response connect(cc.owoo.godpen.network.http.RequestInfo info, Response response) {
        if (response == null)
            response = new Response();
        response.requestClient = this;
        response.requestInfo = info;
        response.requestStartTime = System.currentTimeMillis();
        con:
        if (requestCache != null) {
            byte[] data = requestCache.get(info.getUrl());
            if (data == null)
                break con;
            response.code = 200;
            response.message = "OK";
            response.responseEndTime = System.currentTimeMillis();
            response.content = data;
            return response;
        }
        if (info.host == null)
            return responseCode(response, -101, "域名解析失败", true);
        Socket socket;
        InputStream input;
        OutputStream output;
        try {
            socket = createSocket(info.host, info.port, info.agreement);
            input = socket.getInputStream();
            output = socket.getOutputStream();
            if (throughput != null) {
                input = throughput.count(input);
                output = throughput.count(output);
            }
        } catch (Exception e) {
            return responseCode(response, -102, "建立Socket连接失败：" + e.getMessage(), true);
        }
        try {
            info.write(output);
        } catch (Exception e) {
            return responseCode(response, -103, "发送数据的过程中发生了错误：" + e.getMessage(), true);
        }
        try {
            response.read(input);
        } catch (IOException e) {
            responseCode(response, -104, "读取数据时发生了错误：" + e.getMessage(), false);
        }
        try {
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            response.content = HttpUtil.dataDecode(response.content, response.header.get("content-encoding"));
        } catch (IOException e) {
            responseCode(response, -201, "解析数据时发生了错误：" + e.getMessage(), false);
        }
        response.responseEndTime = System.currentTimeMillis();
        if (requestCache != null)
            requestCache.set(info.getUrl(), response.content);
        con:
        if (isRecordCookie) {
            String string = response.getHeader("set-cookie");
            if (string == null)
                break con;
            getCookie().append(new Cookie().parse(string));
        }
        return response;
    }

    /**
     * 修改response对象的code和message
     *
     * @param response   response对象
     * @param code       状态码
     * @param message    状态信息
     * @param recordTime 是否记录结束时间戳
     * @return response对象
     */
    private Response responseCode(Response response, int code, String message, boolean recordTime) {
        response.code = code;
        response.message = message;
        if (recordTime)
            response.responseEndTime = System.currentTimeMillis();
        return response;
    }

    /**
     * 根据协议创建socket连接
     *
     * @param address   主机地址
     * @param port      端口号
     * @param agreement 协议
     * @return Socket对象
     * @throws IOException 连接出现异常
     */
    private Socket createSocket(InetAddress address, int port, String agreement) throws IOException {
        Proxy proxy;
        if (proxyPool == null)
            proxy = null;
        else {
            proxy = proxyPool.get();
            if (proxy == null)
                throw new NullPointerException("代理池中取出的代理不能为空");
        }
        Socket socket = proxy == null ? new Socket() : new Socket(proxy);
        socket.connect(new InetSocketAddress(address, port), timeout);
        if (agreement.equals("https")) {
            try {
                return createSSLSocket(socket, address, port);
            } catch (Exception e) {
                throw new IllegalArgumentException("SSL加载异常");
            }
        }
        return socket;
    }

    /**
     * 创建SSL Socket
     *
     * @param socket  socket
     * @param address 主机地址
     * @param port    请求端口
     * @return SSLSocket对象
     * @throws IOException 处理异常
     */
    private SSLSocket createSSLSocket(Socket socket, InetAddress address, int port) throws IOException {
        SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, address.getHostName(), port, true);
        sslSocket.startHandshake();
        return sslSocket;
    }

    /**
     * 为字符串添加请求头
     *
     * @param request 请求对象
     * @param info    请求信息
     */
    private void appendHeader(Request request, RequestInfo info) {
        if (info.host == null)
            throw new NullPointerException("域名解析异常，请检查域名是否存在或者后缀是否正确: " + request.getUrl());
        if (!info.containsHeaderKey("host"))
            info.setHeader("host", info.host.getHostName() + ":" + info.port);
        request.forHeader((key, value) -> {
            if (!info.containsHeaderKey(key))
                info.setHeader(key, value);
        });
        forHeader((key, value) -> {
            if (!info.containsHeaderKey(key))
                info.setHeader(key, value);
        });
        if (info.content != null &&
                request.autoContentLength &&
                !request.method.equalsIgnoreCase("get"))
            info.setHeader("content-length", String.valueOf(info.content.length));
    }

    @Override
    public String analysisHost(String url) {
        return super.analysisHost(url);
    }

    @Override
    public String setParam(String key, String value) {
        return super.setParam(key, value);
    }

    @Override
    public String getParam(String key) {
        return super.getParam(key);
    }

    @Override
    public String getUrl() {
        return super.getUrl();
    }

    @Override
    public String getUrl(boolean isAgreement, boolean isHost, boolean isPath, boolean isParams) {
        return super.getUrl(isAgreement, isHost, isPath, isParams);
    }

    @Override
    public int setHeader(String string) {
        return super.setHeader(string);
    }

    @Override
    public boolean setHeader(String key, String value) {
        return super.setHeader(key, value);
    }

    @Override
    public void setAgreement(String agreement) {
        super.setAgreement(agreement);
    }

    @Override
    public boolean setHostString(String host) {
        return super.setHostString(host);
    }

    @Override
    public void setHost(InetAddress host) {
        super.setHost(host);
    }

    @Override
    public void setPort(int port) {
        super.setPort(port);
    }

    @Override
    public void setUrl(String url) {
        super.setUrl(url);
    }

    @Override
    public void setPath(String path) {
        super.setPath(path);
    }

    @Override
    public void setPath(Path path) {
        super.setPath(path);
    }

    @Override
    public Path getPath() {
        return super.getPath();
    }


    @Override
    public void setMethod(String method) {
        super.setMethod(method);
    }

    @Override
    public String getMethod() {
        return super.getMethod();
    }

    @Override
    public boolean isMethod(String method) {
        return super.isMethod(method);
    }

    @Override
    public void setUserAgent(String string) {
        super.setUserAgent(string);
    }

    @Override
    public void setContentType(String string) {
        super.setContentType(string);
    }

    @Override
    public void setCookie(String string) {
        super.setCookie(string);
    }

    @Override
    public void setContent(Json json) {
        super.setContent(json);
    }

    @Override
    public void setContent(byte[] content) {
        super.setContent(content);
    }

    @Override
    public boolean setContent(File file) {
        return super.setContent(file);
    }

    /**
     * 获取连接超时时长
     *
     * @return 连接超时时长
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * 设置连接超时时长
     *
     * @param timeout 连接超时时长
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
}
